Skip to content

fix(cas): fall back from unwritable installation pools - #349

Merged
bobtista merged 9 commits into
developmentfrom
fix/cas-pool-writable-fallback
Aug 3, 2026
Merged

fix(cas): fall back from unwritable installation pools#349
bobtista merged 9 commits into
developmentfrom
fix/cas-pool-writable-fallback

Conversation

@bobtista

@bobtista bobtista commented Aug 2, 2026

Copy link
Copy Markdown

Summary

Prevent installation-adjacent CAS storage from breaking content acquisition when
the game is installed in a protected location such as Program Files.

GenHub now verifies that it can create and write the actual installation-pool
directory before routing content there. When the location is unavailable, new
content uses the primary user-writable pool while existing readable objects
remain discoverable through a read-only legacy pool.

Changes

  • Add a shared, cached storage-writability probe used by both workspace and CAS
    location resolution.
  • Probe the actual target directory, including directory creation, and clean up
    failed probe directories safely.
  • Treat empty, invalid, or unwritable installation pools as unavailable.
  • Route GameInstallation, GameClient, Addon, Patch, Map, and Mod content to the
    primary pool when installation storage is unavailable.
  • Centralize automatic installation-pool selection and persistence.
  • Record whether an installation-pool path was automatically derived.
  • Preserve deliberate custom paths instead of overwriting or clearing them.
  • Repair historical auto-derived paths and remove their obsolete explicit-setting
    marker.
  • Retain every previously used installation-pool root for read-only object lookup
    without copying or deleting its contents. Roots accumulate rather than replacing
    one another, so a pool that moves more than once does not strand the objects
    written to an earlier root.
  • Refresh cached installation and legacy storage when pool settings change,
    while keeping cached lookups free of initialization locks and filesystem checks.
  • Allow CAS reads to inspect existing objects without creating writable
    directories.
  • Report the effective CAS location in Settings and launch diagnostics.
  • Preserve installation and legacy pool settings when applying the default
    primary CAS root.
  • Add coverage for pool routing, provenance, migration, stale-pool removal,
    actual-directory probing, effective display paths, and legacy-content lookup.

Testing

  • dotnet test GenHub/GenHub.Tests/GenHub.Tests.Core/GenHub.Tests.Core.csproj -c Release
    • Passed: 1,537
    • Failed: 0
  • dotnet build GenHub/GenHub.Linux/GenHub.Linux.csproj -c Release
    • Passed
  • Formatting verification and git diff --check
    • Passed
  • GitHub Actions validates Windows, Linux, macOS, and the stable Build Summary.
  • Packaged non-administrator Windows validation:
    • Passed on Windows 11 Home build 26200 using standard user MSI\bobti; elevation check returned False.
    • Tested packaged artifact 0.0.1035-pr349 from run 30774539724, PR head 7fb0a1b.
    • EA App installation was detected under C:\Program Files\EA Games\Command and Conquer Generals Zero Hour; a direct write probe was denied as expected.
    • Acquired TheSuperHackers weekly-2026-07-31 GameClient successfully.
    • New content used C:\Users\bobti\AppData\Roaming\GenHub\cas-pool; InstallationPoolRootPath remained empty.
    • Restart preserved the acquired clients and profiles.
    • Profile workspace was created under C:\Users\bobti\AppData\Local\GenHub\Workspaces.
    • No installation-adjacent .genhub-cas or .genhub-workspace directories were created.
    • SuperHackers - Generals launched successfully from its user-writable workspace.

Risks and rollback

  • Falling back to the primary pool may require cross-volume copying during
    workspace materialization; fix(workspace): fall back from protected adjacent storage #346 provides that copy path.
  • Existing objects are not moved or deleted. A previous readable installation
    pool is retained only for lookup, while new writes use the effective writable
    pool.
  • The new provenance and legacy-path settings are additive and default to the
    prior behavior when absent. LegacyInstallationPoolRootPaths is a list rather
    than a single path, and has not shipped in any release, so no settings
    migration is required.
  • Automatic CAS garbage collection remains disabled by fix: disable unsafe CAS garbage collection #312, so this change does
    not enable mutation of legacy or protected pools.
  • Reverting restores the previous installation-pool selection behavior and its
    protected-path acquisition failures.

Related issues

Fixes #347
Related to #344
Related to #346
Related to #307

Greptile Summary

The PR makes CAS pool selection tolerate protected installation locations while retaining existing content through read-only legacy pools.

  • Adds a shared cached writability probe and falls back to the primary pool when installation-adjacent storage is unavailable.
  • Centralizes installation-pool selection, provenance, migration, persistence, and cache refresh behavior.
  • Retains previous pool roots for object lookup and avoids creating writable directories during reads.
  • Fixes dotted installation-directory handling by treating installation paths according to their directory-path contract.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported dotted-directory pool-selection issue is corrected in both relevant resolution paths and covered by focused tests.

Important Files Changed

Filename Overview
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs Centralizes writable installation-pool derivation, provenance migration, persistence, and legacy-root retention; the previous dotted-directory issue is fixed.
GenHub/GenHub/Common/Services/StorageWritabilityProbe.cs Adds a cached filesystem probe that creates the actual target directory, verifies file writes, and performs safe cleanup.
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs Refreshes active and legacy storage instances when settings change while keeping ordinary cached lookups free of filesystem probing.
GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs Routes installation-oriented content to primary storage when the configured installation pool is unavailable and exposes readable legacy roots.
GenHub/GenHub/Features/Storage/Services/CasService.cs Extends object lookup across retained legacy stores after checking the selected and primary pools.
GenHub/GenHub/Common/Services/StorageLocationService.cs Reports the effective writable CAS location and preserves dotted installation directory components.
GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs Delegates installation-pool preparation to the centralized service and permits primary-pool fallback when installation detection is unavailable.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Detect game installations] --> B[Derive installation-adjacent CAS path]
    B --> C{Writability probe succeeds?}
    C -->|Yes| D[Persist active installation pool]
    C -->|No| E[Route new writes to primary pool]
    D --> F[Write newly acquired content]
    E --> F
    D --> G[Retain previous roots as legacy pools]
    E --> G
    G --> H[Search active, primary, and legacy pools on reads]
Loading

Reviews (7): Last reviewed commit: "fix(cas): retain every previous installa..." | Re-trigger Greptile

Context used (3)

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Automatically selects a writable installation storage location near detected game installations.
    • Preserves access to content in previous installation locations, including read-only legacy storage.
    • Falls back to the primary storage pool when installation-specific locations are unavailable.
    • Improves storage availability detection and cleans up temporary probe artifacts.
  • Bug Fixes
    • Improved content lookup across active, primary, and legacy storage pools.
    • Prevented unnecessary directory creation during content path checks.
    • Prevented content acquisition from proceeding when installation storage setup fails.
    • Preserved existing storage settings when applying default paths.

Walkthrough

The PR adds cached storage writability probing, writable installation CAS selection, legacy-pool retention, fallback CAS lookup, and centralized installation-pool setup. Content services now delegate pool configuration to the installation CAS pool service.

Changes

CAS pool writability and migration

Layer / File(s) Summary
Contracts, configuration, and writability probing
GenHub/GenHub.Core/Interfaces/..., GenHub/GenHub.Core/Models/Storage/CasConfiguration.cs, GenHub/GenHub/Common/Services/StorageWritabilityProbe.cs, GenHub/GenHub/Infrastructure/DependencyInjection/*
Adds writability and installation-pool contracts. Stores auto-derived and legacy paths. Adds cached filesystem probing and registrations.
Installation pool selection and validation
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs, GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs, GenHub/GenHub/Common/Services/StorageLocationService.cs, GenHub/GenHub/Common/Services/ConfigurationProviderService.cs, GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/*, GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/*
Selects writable configured or adjacent paths, preserves explicit settings, tracks provenance, and validates fallback behavior.
Pool lifecycle and legacy content lookup
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs, GenHub/GenHub/Features/Storage/Services/CasService.cs, GenHub/GenHub/Features/Storage/Services/CasStorage.cs
Refreshes active pools, retains readable legacy pools, searches fallback pools, and avoids directory creation during read-only lookup.
Content-service integration
GenHub/GenHub/Features/Content/Services/..., GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs
Delegates installation-pool setup to IInstallationCasPoolService and reports setup failure before manifest storage.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • community-outpost/GenHub#347: The PR implements writable installation-pool fallback, persisted-path migration, and legacy-content retention.
  • community-outpost/GenHub#344: The PR adds writability probing and fallback logic for installation-adjacent storage.

Possibly related PRs

Suggested labels: Bug

Poem

A rabbit checks each storage path,
Then keeps writable pools on track.
Old CAS content stays in view,
New settings carry provenance too.
Content follows the valid route.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address the linked issue's coding requirements for probing, fallback, provenance, migration, legacy lookup, reinitialization, and routing.
Out of Scope Changes check ✅ Passed All summarized changes support the linked CAS fallback, migration, storage probing, routing, or test coverage objectives.
Docstring Coverage ✅ Passed Docstring coverage is 75.41% which is sufficient. The required threshold is 50.00%.
Title check ✅ Passed The title follows Conventional Commits format and clearly describes the CAS fallback change for unwritable installation pools.
Description check ✅ Passed The description directly explains the CAS fallback, writability probing, legacy-pool retention, testing, and related objectives.
📋 Issue Planner

Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).

View plan for ticket: #347

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cas-pool-writable-fallback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added Bug Something isn't working right Testing Topic related to (unit) tests labels Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs (1)

615-647: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Check the result of EnsurePoolPathAsync before proceeding.

EnsureInstallationPoolPathAsync awaits installationCasPoolService.EnsurePoolPathAsync at Line 641 but discards the returned boolean. In ContentOrchestrator.cs, the same call captures the result and fails the operation when it is false (Lines 552-557 of that file). Here, if pool-path resolution fails (for example, no writable pool can be established for any detected installation), DeliverContentAsync still proceeds to call manifestPool.AddManifestAsync for the GameClient manifest at Line 339, so content can be registered without a confirmed writable effective pool.

This contradicts the PR objective to ensure all installation-related content types resolve to a writable effective pool before storage. Change the method to return the success flag, and let the caller in DeliverContentAsync fail fast, consistent with ContentOrchestrator.EnsureInstallationPoolPathAsync.

🐛 Proposed fix to propagate pool-path resolution failure
-    private async Task EnsureInstallationPoolPathAsync(CancellationToken cancellationToken)
+    private async Task<bool> EnsureInstallationPoolPathAsync(CancellationToken cancellationToken)
     {
         try
         {
             // ALWAYS force installation detection and reset the path
             // Even if a path is set, it might be stale (from before user deleted data)
             // or point to the wrong installation
             logger.LogInformation("Forcing installation detection to ensure correct InstallationPoolRootPath");
             installationService.InvalidateCache();

             // Get all installations (this will trigger detection if cache is empty)
             var installationsResult = await installationService.GetAllInstallationsAsync(cancellationToken);
             if (!installationsResult.Success || installationsResult.Data == null)
             {
                 logger.LogWarning("Failed to get installations for CAS pool path resolution: {Error}", installationsResult.FirstError);
-                return;
+                return false;
             }

             var installations = installationsResult.Data.ToList();

             if (installations.Count == 0)
             {
                 logger.LogWarning("No installations detected - cannot set InstallationPoolRootPath");
-                return;
+                return false;
             }

-            await installationCasPoolService.EnsurePoolPathAsync(installations, cancellationToken);
+            return await installationCasPoolService.EnsurePoolPathAsync(installations, cancellationToken);
         }
         catch (Exception ex)
         {
             logger.LogError(ex, "Failed to ensure InstallationPoolRootPath is set");
+            return false;
         }
     }

And at the call site:

             var hasGameClientManifest = manifests.Any(m => m.ContentType == ContentType.GameClient);
             if (hasGameClientManifest)
             {
-                await EnsureInstallationPoolPathAsync(cancellationToken);
+                var poolPathReady = await EnsureInstallationPoolPathAsync(cancellationToken);
+                if (!poolPathReady)
+                {
+                    return OperationResult<ContentManifest>.CreateFailure(
+                        "Could not ensure a writable InstallationPoolRootPath for GameClient content.");
+                }
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs`
around lines 615 - 647, Update EnsureInstallationPoolPathAsync to return the
boolean result from installationCasPoolService.EnsurePoolPathAsync, returning
false on failed installation lookup, no installations, or caught exceptions. In
DeliverContentAsync, check this result before adding the GameClient manifest and
fail fast when pool-path resolution is unsuccessful, matching the existing
ContentOrchestrator behavior.
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs (1)

91-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the GameClient pool-path branch.

AcquireContentAsync_ValidatesAndStoresContent_Successfully uses a manifest with the default ContentType, so it never exercises the new branch at Lines 550-558 of ContentOrchestrator.cs that calls _installationCasPoolService.EnsurePoolPathAsync. Add a test where manifest.ContentType == ContentType.GameClient and _installationCasPoolServiceMock returns false, and assert that AcquireContentAsync returns a failure result without calling AddManifestAsync. This closes a gap called out in the PR objectives for reinitialization and unwritable-pool scenarios.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs`
around lines 91 - 146, Add a dedicated GameClient acquisition test alongside
AcquireContentAsync_ValidatesAndStoresContent_Successfully, configure
manifest.ContentType to ContentType.GameClient and
_installationCasPoolServiceMock.EnsurePoolPathAsync to return false, then assert
AcquireContentAsync returns failure and _manifestPoolMock.AddManifestAsync is
never called. Keep the existing successful default-content test unchanged.
GenHub/GenHub/Common/Services/ConfigurationProviderService.cs (1)

320-333: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Build the default configuration from Clone() to stop field drift.

The new properties are copied correctly. The surrounding projection still duplicates CasConfiguration.Clone() by hand. Each new CAS property must now be added in two places, and GcLockTimeout is already missing here, so this branch silently resets it to the default. Use Clone() and override only CasRootPath.

♻️ Proposed refactor
-            return new CasConfiguration
-            {
-                CasRootPath = defaultPath,
-                InstallationPoolRootPath = casConfig.InstallationPoolRootPath,
-                IsInstallationPoolRootPathAutoDerived = casConfig.IsInstallationPoolRootPathAutoDerived,
-                LegacyInstallationPoolRootPath = casConfig.LegacyInstallationPoolRootPath,
-                EnableAutomaticGc = casConfig.EnableAutomaticGc,
-                HashAlgorithm = casConfig.HashAlgorithm,
-                GcGracePeriod = casConfig.GcGracePeriod,
-                MaxCacheSizeBytes = casConfig.MaxCacheSizeBytes,
-                AutoGcInterval = casConfig.AutoGcInterval,
-                MaxConcurrentOperations = casConfig.MaxConcurrentOperations,
-                VerifyIntegrity = casConfig.VerifyIntegrity,
-            };
+            var defaultConfig = (CasConfiguration)casConfig.Clone();
+            defaultConfig.CasRootPath = defaultPath;
+            return defaultConfig;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Common/Services/ConfigurationProviderService.cs` around lines
320 - 333, Update the CasConfiguration projection in the relevant
ConfigurationProviderService method to create the default configuration via
casConfig.Clone(), then override only CasRootPath with defaultPath. Remove the
manual property-by-property copying so fields such as GcLockTimeout remain
preserved automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@GenHub/GenHub.Core/Interfaces/Common/IStorageWritabilityProbe.cs`:
- Around line 8-13: Update the XML documentation for
IStorageWritabilityProbe.CanCreateStorageAt to explicitly state that a
successful check may create and leave the storage directory on disk. Clarify
that callers should account for this side effect, especially when probing
read-only paths such as CasPoolResolver.GetLegacyInstallationPoolRootPath.

In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/CasPoolWritabilityTests.cs`:
- Around line 153-162: Update
StorageWritabilityProbe_WhenLocationIsWritable_LeavesNoProbeFile to build the
Directory.GetFiles search pattern from StorageConstants.WriteProbeFilePrefix
instead of a hardcoded probe prefix, ensuring the assertion detects leaked
files. Also replace the ".genhub-cas" literals at the referenced setup and test
locations with DirectoryNames.GenHubCasPool, adding the GenHub.Core.Constants
import if needed.

In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs`:
- Line 42: Update the poolPath setup in the affected installation CAS pool tests
to use the shared DirectoryNames.GenHubCasPool constant instead of the hardcoded
".genhub-cas" literal, so assertions follow the same contract as
InstallationCasPoolService.GetDerivedPoolPath.
- Around line 232-236: Update Dispose in the test class to make
temporary-directory cleanup resilient when CasStorage or CasPoolManager leaves
Windows file handles open: catch cleanup-related IOException and
UnauthorizedAccessException from Directory.Delete(_tempPath, true) so teardown
does not mask the test result, while retaining GC.SuppressFinalize(this).

In `@GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs`:
- Around line 219-237: Replace the hand-written CasConfiguration projection in
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs lines 219-237 within
CreateStorage with a clone of _config, then override CasRootPath with rootPath.
Apply the same change in
GenHub/GenHub/Common/Services/ConfigurationProviderService.cs lines 320-333
within GetCasConfiguration: clone casConfig and override CasRootPath with
defaultPath, preserving all configuration properties including GcLockTimeout.
- Around line 239-264: Refactor RefreshInstallationPools so GetStorage does not
acquire _initLock or call Directory.Exists on every lookup. Add an unlocked fast
pre-check comparing the current resolver roots with _installationPoolRoot and
_legacyInstallationPoolRoot, entering the existing locked refresh only when
roots change or cached state requires initialization; cache the legacy-root
availability and re-evaluate it through ReinitializeInstallationPool instead of
RefreshLegacyInstallationPool on every call.
- Around line 118-128: Update GetAllStorages to read _legacyInstallationStorage
once into a local variable after RefreshInstallationPools, then use that local
for the null check, containment check, and add operation. Do not access the
shared field again in this method.

In `@GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs`:
- Around line 79-89: Update the legacy-path branch in the resolver method
containing LegacyInstallationPoolRootPath to return the configured path only
when it is non-empty and Directory.Exists passes; otherwise continue to the
existing InstallationPoolRootPath fallback logic. Keep the current writability
validation and empty-string behavior for the current path unchanged.

In `@GenHub/GenHub/Features/Storage/Services/CasService.cs`:
- Line 577: Change the legacy CAS pool hit logging in both loops within
CasService to use LogDebug instead of LogInformation, including the comparable
log statement around the existing line 558 fallback path, while preserving the
message and hash argument.
- Around line 626-642: Update the fallback loop in ExistsAsync to skip both the
already-checked primaryStorage and the current storage, matching the exclusion
logic in GetContentPathAsync. Preserve fallback checks for all other storages
and the existing exists/break behavior.

In `@GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs`:
- Around line 170-173: Update the installationPath handling in
InstallationCasPoolService so it removes the final path segment only when the
path points to an existing file, rather than using the lexical Path.HasExtension
check. Preserve directory paths containing dots unchanged so pool derivation and
IsAutoDerived receive the correct installation directory.
- Around line 38-70: Update EnsurePoolPathAsync in InstallationCasPoolService so
the branches for no installations, no usable preferred-installation path, and
invalid normalized derived path return true, matching their logged fallback to
the primary CAS pool. Preserve false only when saving settings fails, and leave
the successful derived-pool path unchanged.
- Around line 206-216: Update SelectLegacyPath and
CasConfiguration.LegacyInstallationPoolRootPath to retain a collection of legacy
installation roots rather than a single path. During migration, append newly
discovered valid roots without replacing previously retained roots, and update
legacy-reader and read-only lookup consumers to search the full collection in
order. Preserve existing behavior when no additional legacy root is available.
- Around line 75-88: The historical auto-derived marker in EnsurePoolPathAsync
must be tracked separately from ExplicitlySetProperties. Update the migration
logic around historicalAutoDerivedMarker and IsAutoDerived so an explicitly
user-configured installation pool path is never classified as auto-derived,
replaced, or moved to LegacyInstallationPoolRootPath; remove or clear the
migration marker after it is used.

In `@GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs`:
- Around line 51-52: Remove the explicit StorageWritabilityProbe logger
registration that uses bootstrapLoggerFactory, allowing its
ILogger<StorageWritabilityProbe> dependency to resolve through the main
AddLoggingModule logging pipeline.

---

Outside diff comments:
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs`:
- Around line 91-146: Add a dedicated GameClient acquisition test alongside
AcquireContentAsync_ValidatesAndStoresContent_Successfully, configure
manifest.ContentType to ContentType.GameClient and
_installationCasPoolServiceMock.EnsurePoolPathAsync to return false, then assert
AcquireContentAsync returns failure and _manifestPoolMock.AddManifestAsync is
never called. Keep the existing successful default-content test unchanged.

In `@GenHub/GenHub/Common/Services/ConfigurationProviderService.cs`:
- Around line 320-333: Update the CasConfiguration projection in the relevant
ConfigurationProviderService method to create the default configuration via
casConfig.Clone(), then override only CasRootPath with defaultPath. Remove the
manual property-by-property copying so fields such as GcLockTimeout remain
preserved automatically.

In
`@GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs`:
- Around line 615-647: Update EnsureInstallationPoolPathAsync to return the
boolean result from installationCasPoolService.EnsurePoolPathAsync, returning
false on failed installation lookup, no installations, or caught exceptions. In
DeliverContentAsync, check this result before adding the GameClient manifest and
fail fast when pool-path resolution is unsuccessful, matching the existing
ContentOrchestrator behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8cabc53-af28-467d-86ea-4fd6c9947d18

📥 Commits

Reviewing files that changed from the base of the PR and between 68a662e and 7d7098d.

📒 Files selected for processing (20)
  • GenHub/GenHub.Core/Interfaces/Common/IStorageWritabilityProbe.cs
  • GenHub/GenHub.Core/Interfaces/Storage/ICasPoolResolver.cs
  • GenHub/GenHub.Core/Interfaces/Storage/IInstallationCasPoolService.cs
  • GenHub/GenHub.Core/Models/Storage/CasConfiguration.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/StorageLocationServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/CasPoolWritabilityTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
  • GenHub/GenHub/Common/Services/ConfigurationProviderService.cs
  • GenHub/GenHub/Common/Services/StorageLocationService.cs
  • GenHub/GenHub/Common/Services/StorageWritabilityProbe.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs
  • GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs
  • GenHub/GenHub/Features/Storage/Services/CasService.cs
  • GenHub/GenHub/Features/Storage/Services/CasStorage.cs
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/CasModule.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs
💤 Files with no reviewable changes (1)
  • GenHub/GenHub/Features/Storage/Services/CasStorage.cs

Comment thread GenHub/GenHub.Core/Interfaces/Common/IStorageWritabilityProbe.cs
Comment thread GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs
Comment on lines +170 to +173
if (Path.HasExtension(installationPath))
{
installationPath = Path.GetDirectoryName(installationPath);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Path.HasExtension misclassifies directories whose name contains a dot.

Path.HasExtension is a lexical check only. It returns true for real directory paths such as D:\Games\Command and Conquer Generals Zero Hour 1.04 or D:\Games\C&C.Generals. Line 172 then strips the game folder and returns the parent.

Two failures follow. The derived pool lands beside the game folder instead of inside it. Two installations under one parent, both with dots in the folder name, derive the same pool path. The wrong values also enter derivedPaths at lines 50-55, so IsAutoDerived at line 192 misclassifies a stored path.

The intent is to handle a path that points at an executable. Test the filesystem instead.

🐛 Proposed fix
-        if (Path.HasExtension(installationPath))
-        {
-            installationPath = Path.GetDirectoryName(installationPath);
-        }
+        // Detection may report an executable path; use its containing directory.
+        if (File.Exists(installationPath))
+        {
+            installationPath = Path.GetDirectoryName(installationPath);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Path.HasExtension(installationPath))
{
installationPath = Path.GetDirectoryName(installationPath);
}
// Detection may report an executable path; use its containing directory.
if (File.Exists(installationPath))
{
installationPath = Path.GetDirectoryName(installationPath);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs` around
lines 170 - 173, Update the installationPath handling in
InstallationCasPoolService so it removes the final path segment only when the
path points to an existing file, rather than using the lexical Path.HasExtension
check. Preserve directory paths containing dots unchanged so pool derivation and
IsAutoDerived receive the correct installation directory.

Comment thread GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs Outdated
Comment thread GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs Outdated
/// <inheritdoc/>
public async Task<bool> EnsurePoolPathAsync(
IReadOnlyList<GameInstallation> installations,
CancellationToken cancellationToken = default)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: cancellationToken is accepted but never honored

The token is declared on EnsurePoolPathAsync but never read, and IUserSettingsService.TryUpdateAndSaveAsync does not accept one. ContentOrchestrator and CommunityOutpostDeliverer forward their own cancellation token expecting cooperative cancellation, but the writability probe and settings save here run to completion regardless. Add a ThrowIfCancellationRequested check (at least before the save) or drop the parameter.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale — this was addressed in 4a1d4f0 before this comment's commit was current.

EnsurePoolPathAsync now calls cancellationToken.ThrowIfCancellationRequested() at entry (InstallationCasPoolService.cs:37) and again immediately before the TryUpdateAndSaveAsync call (InstallationCasPoolService.cs:127), which is the point the comment specifically asked for. The parameter is honored, so it stays.

var configuredCurrentPath = currentSettings.CasConfiguration.InstallationPoolRootPath;
var currentPath = NormalizePath(configuredCurrentPath);
var historicalAutoDerivedMarker =
currentSettings.ExplicitlySetProperties.Contains(ExplicitInstallationPoolPathKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Provenance marker is never populated, so these checks are inert in production

ExplicitInstallationPoolPathKey is nameof(CasConfiguration.InstallationPoolRootPath) (InstallationPoolRootPath), a nested property. UserSettingsService.MarkExplicitlySetPropertiesFromJson only marks top-level UserSettings properties, so ExplicitlySetProperties never contains this key when settings are loaded from JSON. Consequently historicalAutoDerivedMarker (line 75), the IsAutoDerived clause (line 191), the settingsAlreadyMatch clause (line 117), and the ExplicitlySetProperties.Remove call (line 129) are no-ops in production, and the "remove obsolete explicit-setting marker" migration never fires. The unit test masks this by adding the key manually. Either populate the marker for nested properties or drop these dead branches.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect — the marker does get populated in production, so the branches are live and the migration is needed.

The analysis only considered MarkExplicitlySetPropertiesFromJson, which maps top-level camelCase keys through ConvertJsonPropertyNameToCSharp. That is not the only way the set is populated:

  1. ExplicitlySetProperties is a plain serialized HashSet<string> on UserSettings (UserSettings.cs:85) with no [JsonIgnore]. It round-trips through JsonSerializer.Deserialize<UserSettings> directly, so whatever was persisted is restored verbatim — no per-key mapper involved.
  2. The currently shipped code on development writes exactly this key. ContentOrchestrator.EnsureInstallationPoolPathAsync calls s.MarkAsExplicitlySet(nameof(s.CasConfiguration.InstallationPoolRootPath)) on both the single-installation and preferred-installation paths (ContentOrchestrator.cs:753 and :777 on development), inside the same TryUpdateAndSaveAsync that persists the settings file.

So any user who has acquired GameClient content on a current build already has "InstallationPoolRootPath" in their persisted explicitlySetProperties. That is precisely the historical auto-derived provenance this PR migrates away — the marker was written by automatic derivation, never by user intent, which is why it is treated as auto-derived rather than as an explicit setting. The unit test adding the key manually reproduces that persisted state rather than masking anything.

Keeping the branches as-is.

return null;
}

if (Path.HasExtension(installationPath))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Path.HasExtension misclassifies directories whose name contains a dot

Path.HasExtension returns true for any path whose final segment contains a dot, not only files. A versioned install folder such as C:\Games\ZeroHour v1.04 is treated as a file, so Path.GetDirectoryName strips the real install directory and the CAS pool is derived one level too high (e.g. C:\Games\.genhub-cas instead of C:\Games\ZeroHour v1.04\.genhub-cas). Prefer checking for a known executable extension or Directory.Exists before stripping.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return casPoolPath;
if (settings.UseInstallationAdjacentStorage)
{
var installationPath = Path.HasExtension(installation.InstallationPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Path.HasExtension misclassifies dotted directory names

Same issue as InstallationCasPoolService.GetDerivedPoolPath: a directory like ...\ZeroHour v1.04 is treated as a file because Path.HasExtension is true for the .04 segment, so Path.GetDirectoryName discards the real install folder and the adjacent pool is resolved one level too high.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

/// <inheritdoc/>
public ICasStorage GetStorage(CasPoolType poolType)
{
RefreshInstallationPools();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Pool refresh now runs on every access under a global lock

GetStorage previously refreshed only for CasPoolType.Installation; it now calls RefreshInstallationPools() for every pool type, and GetAllStorages/EnsureAllPoolsInitialized do the same. RefreshInstallationPools holds _initLock across settings reads and filesystem writability probes, serializing all CAS lookups behind one lock. Probes are cached, but on a cold or slow drive (removable/network) every storage access stalls. Consider skipping the refresh on the common Primary-only path or moving the I/O out of the lock.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return;
}

_legacyInstallationStorage = CreateStorage(legacyRoot);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: App-directory security guard is bypassed for the legacy pool

InitializePool rejects CAS roots inside the application directory, but RefreshLegacyInstallationPool builds the legacy storage via CreateStorage(legacyRoot) directly, skipping that guard. legacyRoot comes from LegacyInstallationPoolRootPath/InstallationPoolRootPath settings, and ICasStorage is not read-only (StoreObjectAsync/DeleteObjectAsync are exposed), so a legacy root that resolves into the app directory creates a CAS storage exactly where the guard forbids it. Apply the same AppContext.BaseDirectory check before constructing the legacy storage.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale — the guard is already applied to the legacy pool.

RefreshLegacyInstallationPool calls IsInsideApplicationDirectory(legacyRoot) at CasPoolManager.cs:280, before CreateStorage(legacyRoot) is ever reached. When it matches, the legacy storage and root are cleared and a Security Block: error is logged — the same treatment InitializePool gives. No legacy CasStorage can be constructed inside the application directory.

@kilo-code-bot

kilo-code-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Incremental commit 7fb0a1b (retain every previous installation pool root) is a clean extension of the legacy-pool design. The single LegacyInstallationPoolRootPath string is replaced by a retained List<string> across the model, resolver, manager, and DI wiring. Per-root validation (active-root, primary-root, and application-directory guards), Directory.Exists filtering, and PathHelper.PathComparer dedup are applied consistently. The lock-free reader field _legacyInstallationStorages stays volatile while _legacyInstallationPoolRoots is only touched under _initLock, and settingsAlreadyMatch correctly short-circuits redundant saves once the retained set stabilizes. The previously flagged primary-root duplication is now guarded. New tests cover multi-root retention and per-root lookup exposure. No new issues introduced.

Files Reviewed (8 files)
  • GenHub/GenHub.Core/Interfaces/Storage/ICasPoolResolver.cs
  • GenHub/GenHub.Core/Models/Storage/CasConfiguration.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/CasModule.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/CasPoolWritabilityTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
Previous Review Summaries (5 snapshots, latest commit 84c836d)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 84c836d)

Status: No Issues Found | Recommendation: Merge

The incremental commits (cca0299 tolerate cleanup failures in installation-pool tests, 84c836d drop a redundant installation-path check) are clean, behavior-preserving refactors. The removed if (string.IsNullOrWhiteSpace(installationPath)) guard was genuinely redundant because the immediately-following ternary already returns null in that case, so deletion changes nothing observable. The new Dispose try/catch around Directory.Delete adds best-effort cleanup for IOException/UnauthorizedAccessException, which only improves test resilience. No new issues were introduced.

Files Reviewed (2 files)
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs

Previous review (commit 140f798)

Status: No Issues Found | Recommendation: Merge

The incremental commits (047d0ab normalize legacy pool roots, 140f798 avoid duplicate primary legacy storage) refactor RefreshLegacyInstallationPool so all three pool roots (legacy, active installation, primary CAS) are normalized with Path.TrimEndingDirectorySeparator/GetFullPath and the legacy root is now excluded when it matches either the active root or the primary root. This correctly prevents the active/primary pool from being retained as a duplicate read-only legacy pool when path formatting differs (e.g. trailing separators). Two new tests cover the duplicate-prevention for both roots, and UserSettingsServiceTests gains a round-trip assertion confirming the installation pool path survives save/load. No new issues were introduced.

Files Reviewed (3 files)
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/UserSettingsServiceTests.cs

Previous review (commit 4a1d4f0)

Status: No Issues Found | Recommendation: Merge

The previously reported cancellationToken warning (carry-forward on InstallationCasPoolService.EnsurePoolPathAsync) is now resolved by commit 4a1d4f0, which adds cancellationToken.ThrowIfCancellationRequested() both at method entry and immediately before the settings save. A new test confirms that cancellation arriving during the writability probe prevents settings persistence and pool reinitialization. No new issues were introduced by the incremental changes.

Files Reviewed (2 files)
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs - cancellation checks added; resolves prior WARNING
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs - new cancellation test

Previous review (commit a885079)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs 34 cancellationToken is accepted but never honored; callers cannot cancel the settings save. (Carry-forward from prior review — unchanged line.)
Files Reviewed (9 files)
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs - 1 carry-forward issue
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs
  • GenHub/GenHub/Features/Storage/Services/CasService.cs
  • GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs
  • GenHub/GenHub/Common/Services/ConfigurationProviderService.cs
  • GenHub/GenHub/Common/Services/StorageLocationService.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs
  • GenHub/GenHub.Core/Interfaces/Common/IStorageWritabilityProbe.cs

Fix these issues in Kilo Cloud

Previous review (commit 7d7098d)

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 6
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs 34 cancellationToken is accepted but never honored; callers cannot cancel the settings save.
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs 76 ExplicitInstallationPoolPathKey marker is never populated for nested properties, so the provenance/migration checks are inert in production.
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs 170 Path.HasExtension misclassifies versioned/dotted install directories, deriving the pool one level too high.
GenHub/GenHub/Common/Services/StorageLocationService.cs 41 Same Path.HasExtension misclassification in adjacent-path resolution.
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs 69 RefreshInstallationPools now runs on every GetStorage/GetAllStorages under a global lock, serializing CAS lookups.
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs 284 App-directory security guard is bypassed for legacy pool creation via direct CreateStorage.
Files Reviewed (16 files)
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs - 3 issues
  • GenHub/GenHub/Common/Services/StorageLocationService.cs - 1 issue
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs - 2 issues
  • GenHub/GenHub/Features/Storage/Services/CasPoolResolver.cs
  • GenHub/GenHub/Features/Storage/Services/CasService.cs
  • GenHub/GenHub/Features/Storage/Services/CasStorage.cs
  • GenHub/GenHub/Common/Services/StorageWritabilityProbe.cs
  • GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs
  • GenHub/GenHub.Core/Models/Storage/CasConfiguration.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/CasModule.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/CasPoolWritabilityTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/StorageLocationServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs

Fix these issues in Kilo Cloud


Reviewed by glm-5.2 · Input: 60.8K · Output: 16.3K · Cached: 961.5K

@coderabbitai coderabbitai Bot removed Bug Something isn't working right Testing Topic related to (unit) tests labels Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs (1)

254-285: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize legacyRoot before comparing it to activeInstallationRoot.

The equality check at Line 257-259 compares the raw, unnormalized legacyRoot against activeInstallationRoot, and only calls Path.GetFullPath(legacyRoot) afterward at Line 266. If the two paths refer to the same directory but differ in trailing separator or case, the check misses the match. The method then creates a second ICasStorage for the same directory and logs it as a retained "legacy" pool, even though it is identical to the active writable Installation pool.

Move the normalization before the comparison.

🐛 Proposed fix
         var legacyRoot = _poolResolver.GetLegacyInstallationPoolRootPath();
-        if (string.IsNullOrWhiteSpace(legacyRoot) ||
-            !Directory.Exists(legacyRoot) ||
-            string.Equals(legacyRoot, activeInstallationRoot, PathHelper.PathComparison))
+        if (string.IsNullOrWhiteSpace(legacyRoot) || !Directory.Exists(legacyRoot))
         {
             _legacyInstallationStorage = null;
             _legacyInstallationPoolRoot = null;
             return;
         }
 
         legacyRoot = Path.GetFullPath(legacyRoot);
+        var normalizedActiveRoot = string.IsNullOrEmpty(activeInstallationRoot)
+            ? string.Empty
+            : Path.GetFullPath(activeInstallationRoot);
+        if (string.Equals(legacyRoot, normalizedActiveRoot, PathHelper.PathComparison))
+        {
+            _legacyInstallationStorage = null;
+            _legacyInstallationPoolRoot = null;
+            return;
+        }
+
         if (IsInsideApplicationDirectory(legacyRoot))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs` around lines 254 -
285, Update RefreshLegacyInstallationPool to normalize legacyRoot with
Path.GetFullPath before comparing it to activeInstallationRoot. Keep the
existing invalid-path checks and ensure the normalized path is used for the
equality check and subsequent storage setup, preventing retention of the active
installation pool as a legacy pool.
GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs (1)

684-712: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate EnsureInstallationPoolPathAsync wrapper logic across two files. Both methods invalidate the installation cache, fetch installations, fall back to success on discovery failure, and delegate to IInstallationCasPoolService.EnsurePoolPathAsync; the shared root cause is that this wrapper was not centralized when both call sites adopted the new service.

  • GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs#L684-L712: extract the invalidate-cache/get-installations/fallback/delegate sequence into a shared helper (either a static extension over IInstallationCasPoolService or a new method on the interface, e.g. EnsureEffectivePoolPathAsync(IGameInstallationService, CancellationToken)), and call it here.
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs#L620-L649: replace this method body with a call to the same shared helper, removing the duplicated logic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs` around lines
684 - 712, The installation cache invalidation, installation retrieval,
fallback, and pool-path delegation are duplicated across
EnsureInstallationPoolPathAsync in
GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs lines 684-712 and
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs
lines 620-649. Extract this sequence into one shared helper or
IInstallationCasPoolService method, preserving its cancellation, fallback, and
error behavior, then replace both method bodies with calls to that helper.
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs (1)

162-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared helper for deriving the installation-adjacent CAS pool path. Both files independently combine an installation path with DirectoryNames.GenHubCasPool to derive the adjacent CAS pool location. Both copies needed the identical dotted-directory-name fix in this same PR, showing the duplication already diverges when only one copy is corrected.

  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs#L162-L178: extract GetDerivedPoolPath's path-selection (InstallationPath/ZeroHourPath/GeneralsPath fallback) plus Path.Combine(..., DirectoryNames.GenHubCasPool) into a shared static helper (for example in PathHelper).
  • GenHub/GenHub/Common/Services/StorageLocationService.cs#L39-L50: call the same shared helper instead of re-deriving Path.Combine(installationPath, DirectoryNames.GenHubCasPool) inline.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs` around
lines 162 - 178, The installation-adjacent CAS pool path derivation is
duplicated and must be centralized. In
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs lines
162-178, move the InstallationPath/ZeroHourPath/GeneralsPath fallback selection
and DirectoryNames.GenHubCasPool combination from GetDerivedPoolPath into a
shared static helper. In GenHub/GenHub/Common/Services/StorageLocationService.cs
lines 39-50, replace the inline Path.Combine derivation with calls to that
helper.
♻️ Duplicate comments (2)
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs (1)

195-217: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

SelectLegacyPath can silently drop an earlier retained legacy pool.

existingLegacyPath is a single string, and CasConfiguration.LegacyInstallationPoolRootPath (see the referenced CasConfiguration snippet) stores only one path. When the preferred installation changes more than once, previousPath becomes the just-superseded currentPath, and if that directory still exists, it overwrites existingLegacyPath outright.

Trace: installation A is active with no legacy pool. The preferred installation changes to B; legacy correctly becomes A. The preferred installation changes again to C; previousPath is now B, and since B's directory exists, legacy becomes B — A is dropped from tracking even though A's directory and its CAS objects remain on disk and readable. This contradicts the PR objective to preserve existing CAS content without silently orphaning it.

Track retained legacy roots as a collection instead of a single string, and append newly discovered roots without discarding previously retained ones. This requires updating CasConfiguration.LegacyInstallationPoolRootPath and its consumers (for example CasPoolResolver.GetLegacyInstallationPoolRootPath, which reads this field as a single string).

♻️ Conceptual direction (not a drop-in diff; touches CasConfiguration.cs and its readers)
// CasConfiguration.cs
public List<string> LegacyInstallationPoolRootPaths { get; set; } = new();
-    private static string SelectLegacyPath(
-        UserSettings settings,
-        string currentPath,
-        string candidatePath,
-        string effectivePath)
-    {
-        var existingLegacyPath = NormalizePath(settings.CasConfiguration.LegacyInstallationPoolRootPath);
-        var previousPath = !string.IsNullOrWhiteSpace(currentPath)
-            ? currentPath
-            : candidatePath;
-
-        if (!string.IsNullOrWhiteSpace(effectivePath) &&
-            string.Equals(previousPath, effectivePath, PathHelper.PathComparison))
-        {
-            return existingLegacyPath.Equals(effectivePath, PathHelper.PathComparison)
-                ? string.Empty
-                : existingLegacyPath;
-        }
-
-        return Directory.Exists(previousPath)
-            ? previousPath
-            : existingLegacyPath;
-    }
+    private static List<string> SelectLegacyPaths(
+        UserSettings settings,
+        string currentPath,
+        string candidatePath,
+        string effectivePath)
+    {
+        var retained = settings.CasConfiguration.LegacyInstallationPoolRootPaths
+            .Select(NormalizePath)
+            .Where(path => !string.IsNullOrWhiteSpace(path) &&
+                !string.Equals(path, effectivePath, PathHelper.PathComparison))
+            .ToList();
+
+        var previousPath = !string.IsNullOrWhiteSpace(currentPath) ? currentPath : candidatePath;
+        if (!string.Equals(previousPath, effectivePath, PathHelper.PathComparison) &&
+            Directory.Exists(previousPath) &&
+            !retained.Contains(previousPath, PathHelper.PathComparer))
+        {
+            retained.Add(previousPath);
+        }
+
+        return retained;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs` around
lines 195 - 217, Replace the single legacy-root value in CasConfiguration with a
collection of retained legacy pool paths, then update SelectLegacyPath and all
consumers such as CasPoolResolver.GetLegacyInstallationPoolRootPath to append
newly superseded existing roots without removing previously retained paths.
Preserve path normalization, comparison, and empty-result behavior while
ensuring repeated installation changes retain every readable legacy pool.
GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs (1)

366-371: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Dispose still has no error handling for Directory.Delete.

Directory.Delete(_tempPath, true) runs unguarded. Several tests in this class (for example lines 200-220, 249-294, 300-332, 338-364) construct CasStorage and CasPoolManager instances without disposing them. If any instance holds an open file handle under _tempPath on Windows, Directory.Delete throws IOException or UnauthorizedAccessException, and the cleanup failure masks the real test result.

This was already flagged in a prior review and remains unresolved in this hunk.

💚 Proposed fix
     /// <inheritdoc/>
     public void Dispose()
     {
-        Directory.Delete(_tempPath, true);
+        try
+        {
+            Directory.Delete(_tempPath, true);
+        }
+        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or DirectoryNotFoundException)
+        {
+            // Temporary directory cleanup must not fail the test run.
+        }
+
         GC.SuppressFinalize(this);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs`
around lines 366 - 371, Update Dispose to handle failures from
Directory.Delete(_tempPath, true) without allowing cleanup exceptions to mask
the test result, while preserving GC.SuppressFinalize(this). Catch the relevant
filesystem exceptions around deletion and ensure cleanup remains safe when
CasStorage or CasPoolManager instances still hold handles.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/UserSettingsServiceTests.cs`:
- Around line 135-165: Strengthen
LoadSettings_AfterSave_PreservesInstallationPoolProvenanceMarker by asserting
that loadedSettings.CasConfiguration.InstallationPoolRootPath equals the
explicitly assigned historical installation path, in addition to the existing
ExplicitlySetProperties assertion.

---

Outside diff comments:
In `@GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs`:
- Around line 684-712: The installation cache invalidation, installation
retrieval, fallback, and pool-path delegation are duplicated across
EnsureInstallationPoolPathAsync in
GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs lines 684-712 and
GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs
lines 620-649. Extract this sequence into one shared helper or
IInstallationCasPoolService method, preserving its cancellation, fallback, and
error behavior, then replace both method bodies with calls to that helper.

In `@GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs`:
- Around line 254-285: Update RefreshLegacyInstallationPool to normalize
legacyRoot with Path.GetFullPath before comparing it to activeInstallationRoot.
Keep the existing invalid-path checks and ensure the normalized path is used for
the equality check and subsequent storage setup, preventing retention of the
active installation pool as a legacy pool.

In `@GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs`:
- Around line 162-178: The installation-adjacent CAS pool path derivation is
duplicated and must be centralized. In
GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs lines
162-178, move the InstallationPath/ZeroHourPath/GeneralsPath fallback selection
and DirectoryNames.GenHubCasPool combination from GetDerivedPoolPath into a
shared static helper. In GenHub/GenHub/Common/Services/StorageLocationService.cs
lines 39-50, replace the inline Path.Combine derivation with calls to that
helper.

---

Duplicate comments:
In
`@GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs`:
- Around line 366-371: Update Dispose to handle failures from
Directory.Delete(_tempPath, true) without allowing cleanup exceptions to mask
the test result, while preserving GC.SuppressFinalize(this). Catch the relevant
filesystem exceptions around deletion and ensure cleanup remains safe when
CasStorage or CasPoolManager instances still hold handles.

In `@GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs`:
- Around line 195-217: Replace the single legacy-root value in CasConfiguration
with a collection of retained legacy pool paths, then update SelectLegacyPath
and all consumers such as CasPoolResolver.GetLegacyInstallationPoolRootPath to
append newly superseded existing roots without removing previously retained
paths. Preserve path normalization, comparison, and empty-result behavior while
ensuring repeated installation changes retain every readable legacy pool.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1c695858-2b30-436b-8024-6c3edaffa14a

📥 Commits

Reviewing files that changed from the base of the PR and between 7d7098d and 4a1d4f0.

📒 Files selected for processing (15)
  • GenHub/GenHub.Core/Interfaces/Common/IStorageWritabilityProbe.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/ConfigurationProviderServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/StorageLocationServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/UserSettingsServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Content/ContentOrchestratorTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/CasPoolWritabilityTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
  • GenHub/GenHub/Common/Services/ConfigurationProviderService.cs
  • GenHub/GenHub/Common/Services/StorageLocationService.cs
  • GenHub/GenHub/Features/Content/Services/CommunityOutpost/CommunityOutpostDeliverer.cs
  • GenHub/GenHub/Features/Content/Services/ContentOrchestrator.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs
  • GenHub/GenHub/Features/Storage/Services/CasService.cs
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs
  • GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs
💤 Files with no reviewable changes (1)
  • GenHub/GenHub/Infrastructure/DependencyInjection/ConfigurationModule.cs

@coderabbitai coderabbitai Bot added Bug Something isn't working right Testing Topic related to (unit) tests labels Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs`:
- Around line 265-269: Update the legacy-root validation in the method
containing legacyRoot and normalizedActiveRoot to also normalize CasRootPath and
reject any legacy root matching either the active installation root or the
primary CasRootPath. Preserve the existing path comparison behavior, and add a
regression test covering LegacyInstallationPoolRootPath equal to CasRootPath.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ab519121-b8d8-44df-989c-e3859dfceef2

📥 Commits

Reviewing files that changed from the base of the PR and between 4a1d4f0 and 047d0ab.

📒 Files selected for processing (3)
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Common/Services/UserSettingsServiceTests.cs
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs

Comment thread GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs Outdated
@bobtista

bobtista commented Aug 2, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot removed the Bug Something isn't working right label Aug 2, 2026
@bobtista

bobtista commented Aug 2, 2026

Copy link
Copy Markdown
Author

Went through the open review threads and verified each against the branch. Two were valid and are now fixed; the rest do not survive checking.

Fixed

  • Dispose can fail the test run on Windows (InstallationCasPoolServiceTests) — valid. Directory.Delete(_tempPath, true) now tolerates IOException and UnauthorizedAccessException, matching the pattern already used in CasPoolWritabilityTests.Dispose. Worth doing precisely because this PR needs packaged Windows validation and a cleanup throw would mask the real assertion. (cca0299)
  • Redundant path checkGetDerivedPoolPath tested string.IsNullOrWhiteSpace(installationPath) twice, once as an early return and again in the following ternary. Dropped the dead branch. (84c836d)

Not applicable

  • cancellationToken accepted but never honored — stale. Addressed in 4a1d4f0: ThrowIfCancellationRequested() runs at entry and again immediately before the settings save.
  • App-directory guard bypassed for the legacy pool — stale. RefreshLegacyInstallationPool applies IsInsideApplicationDirectory(legacyRoot) before CreateStorage, clearing the pool and logging a security block.
  • Check the result of EnsurePoolPathAsync before proceeding (CommunityOutpostDeliverer, flagged as outside-diff so it has no thread to reply to) — stale. The result is captured as poolPathReady and short-circuits with a failure OperationResult when false (CommunityOutpostDeliverer.cs:334-339).
  • Provenance marker is never populated — incorrect. ExplicitlySetProperties is a plain serialized HashSet<string> that round-trips through deserialization without going through the top-level camelCase mapper, and the shipped code on development writes this exact key via MarkAsExplicitlySet(nameof(s.CasConfiguration.InstallationPoolRootPath)). The migration is live, not dead. Detail in the thread.
  • Legacy path returned without an existence check — incorrect. RefreshLegacyInstallationPool checks Directory.Exists at the point of consumption and clears the storage, so no dead pool is retained. Detail in the thread.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs (1)

227-248: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use one availability and root snapshot for installation storage.

RefreshInstallationPools records availability at Line [231], then requests the installation root at Lines [232-234]. CasPoolResolver.GetPoolRootPath(CasPoolType.Installation) checks availability again and returns _config.CasRootPath when that check fails.

If settings or writability changes between these calls, installationPoolAvailable remains true while currentRoot becomes the primary root. Lines [245-248] can then initialize CasPoolType.Installation over the primary CAS pool. This creates duplicate storage with different pool keys and can route installation content to the wrong pool.

Return one effective installation-root snapshot from the resolver, or make InitializePool reject the primary fallback for CasPoolType.Installation. Add a regression test with a changing probe result.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs` around lines 227 -
248, Update RefreshInstallationPools and the installation-root resolution flow
to use one consistent availability/root snapshot, preventing
CasPoolType.Installation from being initialized with the primary CAS root when
availability changes between checks. Prefer returning an effective
installation-root snapshot from CasPoolResolver, or make InitializePool reject a
primary-root fallback for installation pools; add a regression test covering a
changing availability probe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs`:
- Around line 227-248: Update RefreshInstallationPools and the installation-root
resolution flow to use one consistent availability/root snapshot, preventing
CasPoolType.Installation from being initialized with the primary CAS root when
availability changes between checks. Prefer returning an effective
installation-root snapshot from CasPoolResolver, or make InitializePool reject a
primary-root fallback for installation pools; add a regression test covering a
changing availability probe.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 52a4a1f7-5bb7-4263-b2b8-2ce6876bef4f

📥 Commits

Reviewing files that changed from the base of the PR and between 047d0ab and 84c836d.

📒 Files selected for processing (3)
  • GenHub/GenHub.Tests/GenHub.Tests.Core/Features/Storage/InstallationCasPoolServiceTests.cs
  • GenHub/GenHub/Features/Storage/Services/CasPoolManager.cs
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs
💤 Files with no reviewable changes (1)
  • GenHub/GenHub/Features/Storage/Services/InstallationCasPoolService.cs

@coderabbitai coderabbitai Bot added Bug Something isn't working right and removed Testing Topic related to (unit) tests labels Aug 3, 2026
@bobtista
bobtista merged commit b3f5c4a into development Aug 3, 2026
8 checks passed
bobtista added a commit that referenced this pull request Aug 3, 2026
* fix(cas): fall back from an unwritable installation CAS pool

* fix(cas): preserve fallback pool state safely

* fix(cas): harden writable pool fallback

* fix(cas): honor pool selection cancellation

* fix(cas): normalize legacy pool roots

* fix(cas): avoid duplicate primary legacy storage

* test(cas): tolerate cleanup failures in installation pool tests

* refactor(cas): drop a redundant installation-path check

* fix(cas): retain every previous installation pool root for lookup

(cherry picked from commit b3f5c4a)
@kilo-code-bot kilo-code-bot Bot mentioned this pull request Aug 3, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working right

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant